// $Id: multipart.c 85 2011-04-24 09:24:10Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2011 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#include <nio.h>
#include <nio/buffer.h>
#include <nio/handler.h>
#include <niohttp.h>
#include <niohttp/multipart.h>

#include "log.h"

#include "multipart.h"

struct niohandler niohttpd_multipart_handler = {
		niohttpd_multipart_handler_setup,
		niohttpd_multipart_handler_teardown,
		niohttpd_multipart_handler_handle_upstream,
		niohttpd_multipart_handler_handle_downstream,
		niohttpd_multipart_handler_free,
};

struct niohttpd_multipart_handler *niohttpd_multipart_handler_new(void)
{
	int i;
	struct niohttpd_multipart_handler *handler;

	niohttpd_debug("constructing");

	if (!(handler = calloc(1, sizeof(*handler)))) {
		niohttpd_error("failed to calloc");
		goto exception;
	}

	if (!(handler->decoder = niohttp_multipart_decoder_new())) {
		niohttpd_error("failed to create multipart decoder");
		goto exception;
	}

	if (!(handler->encoder = niohttp_multipart_encoder_new())) {
		niohttpd_error("failed to create multipart encoder");
		goto exception;
	}

	for (i = 0; i < sizeof(handler->boundary); i++) {
		handler->boundary[i] = '0' + (random() % 10);
	}

	return handler;

exception:
	niohttpd_multipart_handler_free(handler);
	return NULL;
}

void niohttpd_multipart_handler_free(void *obj)
{
	struct niohttpd_multipart_handler *handler = obj;

	niohttpd_debug("destructing: %p", handler);

	if (!(handler)) {
		return;
	}

	if (handler->encoder) {
		niohttp_multipart_encoder_free(handler->encoder);
	}

	if (handler->decoder) {
		niohttp_multipart_decoder_free(handler->decoder);
	}

	free(handler);
}

int niohttpd_multipart_handler_setup(void *obj, const char *name)
{
	int rc;
	struct niohttpd_multipart_handler *handler = obj;

	if (!handler || !name) {
		niohttpd_error("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niohttpd_debug("setting up: %s", name);

	handler->name = name;
	rc = 0;

finally:
	return rc;
}

int niohttpd_multipart_handler_teardown(void *obj)
{
	int rc;
	struct niohttpd_multipart_handler *handler = obj;

	if (!handler) {
		niohttpd_error("null argument(s)");
		rc = NIOHTTPD_EINVAL;
		goto finally;
	}

	niohttpd_debug("tearing down: %s", handler->name);

	niohttp_multipart_decoder_teardown(handler->decoder);

	niohttp_multipart_encoder_teardown(handler->encoder);

	rc = 0;

finally:
	return rc;
}

#define CONTENT_TYPE "multipart/x-mixed-replace; boundary="

int niohttpd_multipart_handler_handle_upstream(void *obj,
		struct niochannel *channel, void *message)
{
	int rc;
	struct niohttpd_multipart_handler *handler = obj;
	struct niohttp_request *req;
	struct niobuffer *buffer;
	struct niohttp_multipart_message *upstream;
	const char *ctype, *boundary;

	if (!handler || !channel || !message) {
		rc = NIOHTTPD_EINVAL;
		niohttpd_error("null argument(s)");
		goto finally;
	}

	niohttpd_debug("handling upstream: %s", handler->name);

	if (!handler->channel) {
		niohttpd_debug("preparing response: %s", handler->name);

		handler->channel = channel;
		req = message;
		buffer = niochannel_buffer(channel);

		if ((rc = niochannel_keep_alive(channel))) {
			niohttpd_error("failed to keep channel alive: %s",
					nio_strerror(rc));
			goto finally;
		}

		if (!strcmp("POST", niohttp_request_method(req)) &&
				(ctype = niohttp_request_header(req,
				"Content-Type")) && !strncmp(CONTENT_TYPE,
				ctype, strlen(CONTENT_TYPE))) {
			boundary = ctype + strlen(CONTENT_TYPE);

			niohttpd_debug("setting up decoder with boundary: %s",
					boundary);

			if ((rc = niohttp_multipart_decoder_setup(
					handler->decoder, boundary))) {
				niohttpd_error("failed to setup: %s",
						niohttpd_strerror(rc));
				goto finally;
			}

			if ((rc = niohttp_multipart_decoder_parse(
					handler->decoder, buffer, &upstream))) {
				niohttpd_error("failed to parse: %s",
						strerror(rc));
				goto finally;
			}

			if (upstream) {
				if ((rc = niochannel_send_upstream(channel,
						handler->name, upstream))) {
					niohttpd_error("failed to send "
							"upstream: %s",
							nio_strerror(rc));
					rc = NIOHTTPD_EIO;
					goto finally;
				}

				niohttp_multipart_decoder_reset(
						handler->decoder);
			}
		}

		niohttpd_debug("preparing response buffer: %s", handler->name);

		niobuffer_clear(buffer);

		if ((rc = niobuffer_putf(buffer, "%s/%d.%d 200 OK\r\n"
				"Content-Type: "CONTENT_TYPE"%.*s\r\n"
				"\r\n"
				"--%.*s\r\n",
				niohttp_request_protocol(req),
				niohttp_request_major_version(req),
				niohttp_request_minor_version(req),
				sizeof(handler->boundary), handler->boundary,
				sizeof(handler->boundary), handler->boundary))) {
			niohttpd_error("failed to prepare response: %s",
					nio_strerror(rc));
			goto finally;
		}

		niobuffer_flip(buffer);

		if ((rc = niochannel_send_downstream(channel,
				handler->name, buffer))) {
			niohttpd_error("failed to send downstream: %s",
					nio_strerror(rc));
			goto finally;
		}
	} else {
		buffer = message;

		if ((rc = niohttp_multipart_decoder_parse(
				handler->decoder, buffer, &upstream))) {
			niohttpd_error("failed to parse: %s",
					strerror(rc));
			goto finally;
		}

		if (upstream) {
			if ((rc = niochannel_send_upstream(channel,
					handler->name, &upstream))) {
				niohttpd_error("failed to send upstream: %s",
						nio_strerror(rc));
				rc = NIOHTTPD_EIO;
				goto finally;
			}

			niohttp_multipart_decoder_reset(handler->decoder);
		}
	}

	rc = 0;

finally:
	return rc;
}

int niohttpd_multipart_handler_handle_downstream(void *obj,
		struct niochannel *channel, void *message)
{
	int rc;
	struct niohttpd_multipart_handler *handler = obj;
	struct niohttp_multipart_message *multipart = message;
	struct niobuffer *boundary;

	if (!handler || !channel || !message) {
		rc = NIOHTTPD_EINVAL;
		niohttpd_error("null argument(s)");
		goto finally;
	}

	niohttpd_debug("handling downstream: %s", handler->name);

	niohttpd_debug("pushing %d bytes of part",
			multipart->content ? niobuffer_remaining(
			multipart->content) : 0);

	boundary = niochannel_buffer(handler->channel);
	niobuffer_clear(boundary);

	if ((rc = niobuffer_putf(boundary, "--%.*s\r\n",
			sizeof(handler->boundary), handler->boundary))) {
		niohttpd_error("failed to append end of part: %s",
				nio_strerror(rc));
		goto finally;
	}

	niobuffer_flip(boundary);

	if ((rc = niohttp_multipart_encoder_setup(handler->encoder,
			multipart->headers))) {
		niohttpd_error("failed to setup: %s",
				niohttpd_strerror(rc));
		goto finally;
	}

	if (!(handler->header = niobuffer_slice(boundary,
			niobuffer_limit(boundary),
			niobuffer_capacity(boundary) -
			niobuffer_limit(boundary)))) {
		niohttpd_error("failed to slice header buffer");
		rc = -1;
		goto finally;
	}

	if ((rc = niohttp_multipart_encoder_format(handler->encoder,
			handler->header))) {
		niohttpd_error("failed to format headers: %s",
				niohttpd_strerror(rc));
		goto finally;
	}

	niobuffer_flip(handler->header);

	if (!(handler->wrapper = niobuffer_wrapper())) {
		niohttpd_error("failed to create wrapper");
		rc = NIOHTTPD_ENOMEM;
		goto finally;
	}

	if ((rc = niobuffer_wrap(handler->wrapper, handler->header))) {
		niohttpd_error("failed to wrap header: %s", nio_strerror(rc));
		goto finally;
	}

	if ((rc = niobuffer_wrap(handler->wrapper, multipart->content))) {
		niohttpd_error("failed to wrap content: %s", nio_strerror(rc));
		goto finally;
	}

	if ((rc = niobuffer_wrap(handler->wrapper, boundary))) {
		niohttpd_error("failed to wrap boundary: %s", nio_strerror(rc));
		goto finally;
	}

	if ((rc = niochannel_send_downstream(handler->channel, handler->name,
			handler->wrapper))) {
		niohttpd_error("failed to send downstream: %s",
				nio_strerror(rc));
		rc = NIOHTTPD_EIO;
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}
